-module(ectmake).
-include_lib("kernel/include/file.hrl").
-export([make/0, make_sh/0]).

-include("./config.hrl").

-define(SRC_DIR,"src/").
-define(BIN_DIR,"ebin/").
-define(TMP_BINDIR,"make.dir/tmpbin/").

-define(INCLUDES,[{i,".."}, {i, "include/"}]).
%-define(COMMON_OPTIONS, [{verbose, true},{d,'ECT_UPGRADE_MODE',mode1}]).
%-define(COMMON_OPTIONS, [{verbose, true}]).
-define(ERLC_OPTIONS,[{outdir, ?BIN_DIR}]++?INCLUDES++?ECT_COMMON_OPTIONS).
-define(ERLC_TMP_ECT_OPTIONS,[{cctrans, cctrans0}, {outdir, ?TMP_BINDIR}]++?INCLUDES++?ECT_COMMON_OPTIONS).
-define(ERLC_ECT_OPTIONS,[{outdir, ?BIN_DIR}]++?INCLUDES++?ECT_COMMON_OPTIONS).

-define(ECT_MODULES,[idtrans,cctrans]).

make_sh() ->
    case make() of
    ok -> erlang:halt();
    error -> erlang:halt(1)
    end.

make() ->
    try
        make0(),
        ok
    catch
        throw:{ectmake, X} ->
            io:format(X++"~n", []),
        error
    end.

make0() ->
    {SimpleToDos, ECTToDos} = collect_changes(),
    build_simple(SimpleToDos),
    case {SimpleToDos, ECTToDos} of
    {[], []} ->
        ok;
    {_, _} ->
        build_ect()
    end.

compile({Source, Module, _}, Options) ->
    Options2 = [report|Options],
    io:format("[MAKE ~p] compile:file(~p, ~1000p).~n", [Module, Source, Options2]),
    case compile:file(Source, Options2) of
    {ok,_} ->
        io:nl(),
        ok;
    error ->
        throw({ectmake, io_lib:format("[MAKE ~p] aborted", [Module])})
    end.

build_simple([Item|SimpleToDos]) ->
    compile(Item, ?ERLC_OPTIONS),
    build_simple(SimpleToDos);
build_simple([]) ->
    ok.

soft_delete([]) ->
    ok;
soft_delete([F|Files]) ->
    case file:delete(F) of
    ok -> ok;
    {error, enoent} -> ok
    end,
    soft_delete(Files).

build_ect() ->
    case filelib:is_dir("make.dir/tmpbin") of
    true -> ok;
    false -> throw({ectmake, "[MAKE] directory make.dir/tmpbin must exist before invoking the Erlang VM for ectmake~n[MAKE] aborted"})
    end,
    
    lists:map(
        fun (Module) ->
            ModuleName = atom_to_list(Module),
            soft_delete(
                [?TMP_BINDIR++ModuleName++".beam"
                ,?TMP_BINDIR++ModuleName++"_impl1.beam"
                ,?TMP_BINDIR++ModuleName++"_proxy1.beam"
                ,?BIN_DIR++ModuleName++".beam"
                ,?BIN_DIR++ModuleName++"_impl1.beam"
                ,?BIN_DIR++ModuleName++"_proxy1.beam"
                ])
        end, ?ECT_MODULES),
    build_ect(?ECT_MODULES, ?ERLC_TMP_ECT_OPTIONS),
    
    % make sure the versions from ?TMP_BINDIR will be used
    lists:map(
        fun (Module) ->
            Module2 = list_to_atom(atom_to_list(Module)++"_impl1"),
            code:delete(Module), code:delete(Module2),
            case code:load_file(Module) of
            {module, Module} -> ok;
            _ -> throw("error loading "++atom_to_list(Module))
            end,
            case code:load_file(Module2) of
            {module, Module2} -> ok;
            _ -> throw("error loading "++atom_to_list(Module2))
            end
        end, ?ECT_MODULES),

    build_ect(?ECT_MODULES, ?ERLC_ECT_OPTIONS).

build_ect([], _) ->
    ok;
build_ect([Item|List], Options) ->
    compile({?SRC_DIR++atom_to_list(Item)++".erl", Item, xxx}, Options),
    build_ect(List, Options).

collect_changes() ->
    IncludePath = lists:map(fun ({i, Path}) -> Path end, ?INCLUDES),
    SourceFiles = filelib:wildcard(?SRC_DIR++"*.erl"),
    CheckList = 
        lists:map(
            fun (Path) ->
                ModuleName = filename:basename(Path, ".erl"),
                {Path
                ,list_to_atom(ModuleName)
                ,?BIN_DIR++ModuleName++".beam"
                }
            end,
            SourceFiles),
    ToDoList = %CheckList,
        lists:filter(
            fun ({Source, _, Beam}) -> is_modified(Source, Beam, IncludePath) end,
            CheckList),
    lists:partition(
        fun({_, Module, _}) ->
            not lists:member(Module, ?ECT_MODULES)
        end,
        ToDoList).

is_modified(Source, Beam, IncludePath) ->
    case {filelib:last_modified(Source), filelib:last_modified(Beam)} of
        {0, _} ->
            throw({ectmake, "source does not exist"});
        {_, 0} ->
            true;
        {SrcDate, BeamDate} when SrcDate > BeamDate ->
            true;
        {_, BeamDate} ->
            check_includes(Source, IncludePath, BeamDate)
    end.


%%% BELOW CODE IS TAKEN FROM Erlang Source Code  (lib/tools/src/make.erl)

%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 1996-2009. All Rights Reserved.
%%
%% The contents of this file are subject to the Erlang Public License,
%% Version 1.1, (the "License"); you may not use this file except in
%% compliance with the License. You should have received a copy of the
%% Erlang Public License along with this software. If not, it can be
%% retrieved online at http://www.erlang.org/.
%%
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.


%%% If you an include file is found with a modification
%%% time larger than the modification time of the object
%%% file, return true. Otherwise return false.
check_includes(File, IncludePath, ObjMTime) ->
    Path = [filename:dirname(File)|IncludePath],
    case epp:open(File, Path, []) of
        {ok, Epp} ->
            check_includes2(Epp, File, ObjMTime);
        _Error ->
            false
    end.

check_includes2(Epp, File, ObjMTime) ->
    case epp:parse_erl_form(Epp) of
        {ok, {attribute, 1, file, {File, 1}}} ->
            check_includes2(Epp, File, ObjMTime);
        {ok, {attribute, 1, file, {IncFile, 1}}} ->
            case file:read_file_info(IncFile) of
                {ok, #file_info{mtime=MTime}} when MTime>ObjMTime ->
                    epp:close(Epp),
                    true;
                _ ->
                    check_includes2(Epp, File, ObjMTime)
            end;
        {ok, _} ->
            check_includes2(Epp, File, ObjMTime);
        {eof, _} ->
            epp:close(Epp),
            false;
        {error, _Error} ->
            check_includes2(Epp, File, ObjMTime)
    end.
